{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Layers and Blocks "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One of the key components that helped propel deep learning is powerful software. In an analogous manner to semiconductor design where engineers went from specifying transistors to logical circuits to writing code we now witness similar progress in the design of deep networks. The previous chapters have seen us move from designing single neurons to entire layers of neurons. However, even network design by layers can be tedious when we have 152 layers, as is the case in ResNet-152, which was proposed by He et al. in 2016 for computer vision problems. Such networks have a fair degree of regularity and they consist of blocks of repeated (or at least similarly designed) layers. These blocks then form the basis of more complex network designs. In short, blocks are combinations of one or more layers. This design is aided by code that generates such blocks on demand, just like a Lego factory generates blocks which can be combined to produce terrific artifacts.\n",
    "We start with very simple block, namely the block for a multilayer perceptron. A common strategy would be to construct a two-layer network as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[-0.1113,  0.0394, -0.4274,  0.1700, -0.5627,  0.1780,  0.0306, -0.2934,\n",
       "          0.1738,  0.0487],\n",
       "        [-0.0080,  0.1829, -0.1894, -0.1051, -0.2702,  0.0274,  0.0572, -0.2681,\n",
       "          0.2906, -0.3333]], grad_fn=<AddmmBackward>)"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "from torch.autograd import Variable\n",
    "import torch.nn.functional as F\n",
    "net = nn.Sequential(nn.Linear(20, 256),nn.ReLU(),nn.Linear(256, 10))\n",
    "x = torch.randn(2,20)\n",
    "net1(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Blocks are combinations of one or more layers.The above code generates a network with a hidden layer of 256 units, followed by a ReLU activation and another 10 units governing the output. In particular, we used the nn.Sequential constructor to generate an empty network into which we then inserted both layers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"http://d2l.ai/_images/blocks.svg\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from IPython.display import Image\n",
    "from IPython.core.display import HTML \n",
    "Image(url= \"http://d2l.ai/_images/blocks.svg\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the following we will explain the various steps needed to go from defining layers to defining blocks (of one or more layers). To get started, we need a bit of reasoning about software. For most intents and purposes a block behaves very much like a fancy layer. That is, it provides the following functionality:\n",
    "\n",
    "1. It needs to ingest data (the input).\n",
    "1. It needs to produce a meaningful output. This is typically encoded in what we will call the forward function. It allows us to invoke a block via net(X) to obtain the desired output. What happens behind the scenes is that it invokes forward to perform forward propagation.\n",
    "1. It needs to produce a gradient with regard to its input when invoking backward. Typically this is automatic.\n",
    "1. It needs to store parameters that are inherent to the block. For instance, the block above contains two hidden layers, and we need a place to store parameters for it.\n",
    "1. Obviously it also needs to initialize these parameters as needed."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## A Custom Block"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The nn.Module class provides the functionality required for much of what we need. It is a model constructor provided in the nn module, which we can inherit to define the model we want. The following inherits the Block class to construct the multilayer perceptron mentioned at the beginning of this section. The MLP class defined here overrides the __init__ and forward functions of the Block class. They are used to create model parameters and define forward computations, respectively. Forward computation is also forward propagation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class MLP(nn.Module):\n",
    "    # Declare a layer with model parameters. Here, we declare two fully\n",
    "    # connected layers\n",
    "    def __init__(self, **kwargs):\n",
    "        # Call the constructor of the MLP parent class Module to perform the\n",
    "        # necessary initialization. In this way, other function parameters can\n",
    "        # also be specified when constructing an instance, such as the model\n",
    "        # parameter, params, described in the following sections\n",
    "        super(MLP, self).__init__(**kwargs)\n",
    "        self.hidden = nn.Sequential(nn.Linear(20,256),nn.ReLU())  # Hidden layer\n",
    "        self.output = nn.Linear(256,10)  # Output layer\n",
    "\n",
    "    # Define the forward computation of the model, that is, how to return the\n",
    "    # required model output based on the input x\n",
    "    def forward(self, x):\n",
    "        return self.output(self.hidden(x))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let’s look at it a bit more closely. The forward method invokes a network simply by evaluating the hidden layer self.hidden(x) and subsequently by evaluating the output layer self.output( ... ). This is what we expect in the forward pass of this block.\n",
    "\n",
    "In order for the block to know what it needs to evaluate, we first need to define the layers. This is what the __init__ method does. It first initializes all of the Block-related parameters and then constructs the requisite layers. This attached the corresponding layers and the required parameters to the class. Note that there is no need to define a backpropagation method in the class. The system automatically generates the backward method needed for back propagation by automatically finding the gradient. The same applies to the initialize method, which is generated automatically. Let’s try this out:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[ 0.2891,  0.0684,  0.2082,  0.0836, -0.3944, -0.1087,  0.0945, -0.0766,\n",
       "          0.4105, -0.2657],\n",
       "        [-0.3127, -0.0016,  0.0645, -0.2138, -0.3153, -0.0401,  0.4539, -0.5397,\n",
       "          0.4545,  0.4328]], grad_fn=<AddmmBackward>)"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net1 = MLP()\n",
    "x = torch.randn(2,20)\n",
    "net1(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## A Sequential Block"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The Block class is a generic component describing dataflow. In fact, the Sequential class is derived from the Block class: when the forward computation of the model is a simple concatenation of computations for each layer, we can define the model in a much simpler way. The purpose of the Sequential class is to provide some useful convenience functions. In particular, the add method allows us to add concatenated Block subclass instances one by one, while the forward computation of the model is to compute these instances one by one in the order of addition. Below, we implement a MySequential class that has the same functionality as the Sequential class. This may help you understand more clearly how the Sequential class works."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class MySequential(nn.Sequential):\n",
    "    def __init__(self, **kwargs):\n",
    "        super(MySequential, self).__init__(**kwargs)\n",
    "\n",
    "    def add_module(self, block):\n",
    "        # Here, block is an instance of a Block subclass, and we assume it has  \n",
    "        # a unique name. We save it in the member variable _children of the\n",
    "        # Block class, and its type is OrderedDict. When the MySequential\n",
    "        # instance calls the initialize function, the system automatically\n",
    "        # initializes all members of _children\n",
    "        self._modules[block] = block\n",
    "        \n",
    "\n",
    "    def forward(self, x):\n",
    "        # OrderedDict guarantees that members will be traversed in the order\n",
    "        # they were added\n",
    "        for block in self._modules.values():\n",
    "            x = block(x)\n",
    "        return x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "At its core is the add method. It adds any block to the ordered dictionary of children. These are then executed in sequence when forward propagation is invoked. Let’s see what the MLP looks like now."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[-0.3895, -0.2349, -0.0982, -0.1948,  0.2431,  0.2273,  0.3051, -0.5656,\n",
       "          0.2300,  0.2857],\n",
       "        [-0.0405, -0.1966, -0.0178, -0.4111,  0.3842,  0.2372,  0.1086, -0.1815,\n",
       "          0.2160,  0.1004]], grad_fn=<AddmmBackward>)"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net = MySequential()\n",
    "net.add_module(nn.Linear(20,256))\n",
    "net.add_module(nn.ReLU())\n",
    "net.add_module(nn.Linear(256,10))\n",
    "x = torch.randn(2,20)\n",
    "net(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    " # Blocks with Code"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although the Sequential class can make model construction easier, and you do not need to define the forward method, directly inheriting the Block class can greatly expand the flexibility of model construction. In particular, we will use Python’s control flow within the forward method. While we’re at it, we need to introduce another concept, that of the constant parameter. These are parameters that are not used when invoking backprop. This sounds very abstract but here’s what’s really going on. Assume that we have some function\n",
    "\n",
    "\n",
    "$$f(x,w)=3⋅w⊤x.$$\n",
    " \n",
    "In this case 3 is a constant parameter. We could change 3 to something else, say  c  via\n",
    "\n",
    "$$f(x,w)=c⋅w⊤x.$$\n",
    "\n",
    "Nothing has really changed, except that we can adjust the value of c.It is still a constant as far as __w__ and __x__ are concerned. However, since Pytorch doesn’t know about this beforehand, it’s worth while to give it a hand (this makes the code go faster, too, since we’re not sending the Pytorch engine on a wild goose chase after a parameter that doesn’t change). \n",
    "Let’s see what this looks like in practice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class FancyMLP(nn.Sequential):\n",
    "    def __init__(self, **kwargs):\n",
    "        super(FancyMLP, self).__init__(**kwargs)\n",
    "        \n",
    "        # Random weight parameters created with the get_constant are not\n",
    "        # iterated during training (i.e. constant parameters)\n",
    "        # self.rand_weight = self.parameters.get_constant('rand_weight', nd.random.uniform(shape=(20, 20)))\n",
    "        # In pytorch you can use any module block if incoming input shape is same with defined block\n",
    "    \n",
    "        \n",
    "        self.dense1=nn.Sequential(nn.Linear(20,20),nn.ReLU())\n",
    "        \n",
    "        self.rand_weight=nn.Parameter(torch.empty(20,20).uniform_(0, 1) )\n",
    "    \n",
    "        self.dense = nn.Sequential(nn.Linear(20,256),nn.ReLU())\n",
    "        \n",
    "        self.register_buffer('random_weights', self.rand_weight)\n",
    "        \n",
    "    def forward(self, x):\n",
    "        x = self.dense1(x)\n",
    "        # Use the constant parameters created, as well as the relu and dot\n",
    "        # functions of NDArray\n",
    "        print(x.shape)\n",
    "        # in pytorch dot product is for 1d tensors \n",
    "        # for 2d tensors use torch.mm or torch.bmm\n",
    "        x = F.relu(torch.mm(x, Variable(self.random_weights).data) + 1)\n",
    "        # Reuse the fully connected layer. This is equivalent to sharing\n",
    "        # parameters with two fully connected layers\n",
    "        x = self.dense(x)\n",
    "        # Here in Control flow, we need to call asscalar to return the scalar\n",
    "        # for comparison\n",
    "        while x.norm().item() > 1:\n",
    "            x /= 2\n",
    "        if x.norm().item() < 0.8:\n",
    "            x *= 10\n",
    "        return x.sum()\n",
    "        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this FancyMLP model, we used constant weight Rand_weight (note that it is not a model parameter), performed a matrix multiplication operation (nd.dot<), and reused the same Dense layer. Note that this is very different from using two dense layers with different sets of parameters. Instead, we used the same network twice. Quite often in deep networks one also says that the parameters are tied when one wants to express that multiple parts of a network share the same parameters. Let’s see what happens if we construct it and feed data through it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([2, 20])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "tensor(10.6916, grad_fn=<SumBackward0>)"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net = FancyMLP()\n",
    "x = torch.randn(2,20)\n",
    "net(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There’s no reason why we couldn’t mix and match these ways of build a network. Obviously the example below resembles more a chimera, or less charitably, a Rube Goldberg Machine. That said, it combines examples for building a block from individual blocks, which in turn, may be blocks themselves. Furthermore, we can even combine multiple strategies inside the same forward function. To demonstrate this, here’s the network."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([2, 20])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "tensor(73.4115, grad_fn=<SumBackward0>)"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class NestMLP(nn.Sequential):\n",
    "    def __init__(self, **kwargs):\n",
    "        super(NestMLP, self).__init__(**kwargs)\n",
    "        self.net = nn.Sequential(nn.Linear(20,64),nn.ReLU(),nn.Linear(64, 32),nn.ReLU())\n",
    "        self.dense = nn.Sequential(nn.Linear(32,20),nn.ReLU())\n",
    "\n",
    "    def forward(self, x):\n",
    "        return self.dense(self.net(x))\n",
    "\n",
    "chimera = nn.Sequential()\n",
    "chimera.add_module(\"Linear1\",nn.Linear(20,20))\n",
    "chimera.add_module(\"NestNlp\",NestMLP())\n",
    "chimera.add_module(\"FancyMLP\",FancyMLP())\n",
    "\n",
    "chimera(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Compilation "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "The avid reader is probably starting to worry about the efficiency of this. After all, we have lots of dictionary lookups, code execution, and lots of other Pythonic things going on in what is supposed to be a high performance deep learning library. The problems of Python’s Global Interpreter Lock are well known. In the context of deep learning it means that we have a super fast GPU (or multiple of them) which might have to wait until a puny single CPU core running Python gets a chance to tell it what to do next. This is clearly awful and there are many ways around it. The best way to speed up Python is by avoiding it altogether.\n",
    "\n",
    "Pytorch does this by allowing for Hybridization. In it, the Python interpreter executes the block the first time it’s invoked. The Pytorch runtime records what is happening and the next time around it short circuits any calls to Python. This can accelerate things considerably in some cases but care needs to be taken with control flow. We suggest that the interested reader skip forward to the section covering hybridization and compilation after finishing the current chapter."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Summary"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Layers are blocks\n",
    "\n",
    "- Many layers can be a block\n",
    "\n",
    "- Many blocks can be a block\n",
    "\n",
    "- Code can be a block\n",
    "\n",
    "- Blocks take are of a lot of housekeeping, such as parameter initialization, backprop and related issues.\n",
    "- Sequential concatenations of layers and blocks are handled by the eponymous Sequential block."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. What kind of error message will you get when calling an __init__ method whose parent class not in the __init__ function of the parent class?\n",
    "1. What kinds of problems will occur if you remove the item function in the FancyMLP class?\n",
    "1. What kinds of problems will occur if you change self.net defined by the Sequential instance in the NestMLP class to self.net = [nn.Sequential(nn.Linear(64,256),nn.ReLU()), nn.Sequential(nn.Linear(32,256),nn.ReLU())]?\n",
    "1. Implement a block that takes two blocks as an argument, say net1 and net2 and returns the concatenated output of both networks in the forward pass (this is also called a parallel block).\n",
    "1. Assume that you want to concatenate multiple instances of the same network. Implement a factory function that generates multiple instances of the same block and build a larger network from it."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
